"""RFB protocol base.

Not dependent on wx.

Implemented according to rfb protocol described in

{{{
The RFB Protocol
Tristan Richardson
RealVNC Ltd
(formerly of Olivetti Research Ltd / AT&T Labs Cambridge)

Version 3.8
Last updated 28 August 2008
}}} paper.

(http://www.realvnc.com/docs/rfbproto.pdf)

"""

import socket
from select import select

from . import (
    encodings,
    exceptions,
    initStates,
    types,
)


class RfbClient(object):
    """Base rfb client implementation.

    Essentially it is state machine.

    States are callable objects to whom this state machine is provided.
    States return next state after execution.

    """

    active = False
    initialized = False

    # type packing/unpacking/couning object will be stored here
    types = None

    _curState = None
    _dataSocket = None
    _dataBuffer = ""

    # default start state (while not attached to socket)
    # is kind of null state -- does nothing and returns itself.
    _startState = lambda client: client._startState

    # implemented protocol version -- (major, minor)
    protocolVersion = (3, 8)

    # current active pixel format
    activePixelFormat = None

    # framebuffer dimensions (w, h)
    fbDimensions = None

    lastAskTime = -1

    def __init__(self):
        self._curState = self._startState
        self.types = types.TypeObject(self)

    def attachToSocket(self, socket):
        """Attach gien rfb client to given socket.

        Socket must be in non-blocking mode.
        """
        self._dataSocket = socket
        self.active = True
        self.initialized = False
        self._curState = initStates.getStartState()

    def detach(self):
        self._dataSocket = None
        self.active = False
        self.initialized = False
        self._curState = self._startState

#### I/O

    def sendData(self, data):
        """Send data to socket."""
        assert self.active
        try:
            while data:
                _sent = self._dataSocket.send(data)
                data = data[_sent:]
        except:
            self.active = False
            raise

    def _recvData(self):
        """Receive all incoming data into client's buffer."""
        assert self.active
        _sock = self._dataSocket
        try:
            (_readable, _, _) = select((_sock, ), (), (), 0)
            while _readable:
                self._dataBuffer += self._dataSocket.recv(2**12)
                (_readable, _, _) = select((_sock, ), (), (), 0)
        except:
            self.active = False
            raise

    def getInputBufferSize(self):
        """Return byte count collected in client's buffer"""
        return len(self._dataBuffer)

    def recvData(self, size, strict=True):
        """Return given amount of data from input buffer.

        If `strict` is True, than exception is raised when there is no
        enough data.
        If `strict` is False, closest possible amount of data
        is returned.

        """
        if strict and len(self._dataBuffer) < size:
            raise exceptions.CommunicationError(
                "Not enough data in buffer")
        _rv = self._dataBuffer[:size]
        self._dataBuffer = self._dataBuffer[size:]
        return _rv

    def act(self):
        """This method must be called from time to time for this object
        actually do something.
        """
        if not self.active:
            return
        self._recvData()
        try:
            _newState = self._curState(self)
        except:
            self.log("Error in state %s" % self._curState)
            raise
        if not callable(_newState) and self.active:
            raise Exception(
                "State %r returned non-callable state %r." % (
                    self._curState, _newState))
        self._curState = _newState

    def log(self, msg):
        """Send message to program's logging facility.

        For override in subclasses.

        """
        print(">>>> " + msg)

    def askSharedSession(self):
        """Return True if client asks server for shared RFB session.

        (False otherwise).

        """
        return True

    def getPixelFormat(self):
        """Return preferred pixel format as dictionary.

        (see key names in rfb_types._structDefs).

        Return False to acceps server's idea of pixel format.

        """
        return None

    def onFbDimensionsSet(self, width, height):
        """Callback called upon framebuffer size receiving from server."""
        self.log("Framebuffer size set (width=%s, height=%s)" % (
            width, height))
        self.fbDimensions = (width, height)

    def setActivePixelFormat(self, fmtDict):
        """Callback on active format set/change."""
        self.activePixelFormat = fmtDict.copy()

    def nameSet(self, name):
        """Called when server sets it's name."""
        self.log("Server name: %r" % name)

    def getBits(self):
        """Return bits-per pixel value."""
        return self.activePixelFormat["bits-per-pixel"]

    def getBpp(self):
        """Return bytes per pixel count."""
        _bits = self.activePixelFormat["bits-per-pixel"]
        assert _bits % 8 == 0
        return _bits / 8

    def onIdle(self):
        """Executed when client is in idling state."""

 ## client-to-server messages

    def setPixelFormat(self, pixelFmt):
        """Set new pixel format."""
        if not self.initialized:
            return
        self.sendData(self.types.pack(
            ("U8", "x", "x", "x", "PIXEL_FORMAT"),
            (0, pixelFmt)))

    def fbUpdateRequest(self, x, y, width, height, incremental=True):
        """Framebuffer update request.

        Asks server to resent givent rectangle.
        incremental = True forces server to resend all data explicitly.

        """
        if not self.initialized:
            return
        self.sendData(self.types.pack(
            ("U8", "U8", "U16", "U16", "U16", "U16"),
            (3, 1 if incremental else 0, x, y, width, height)
        ))

    def screenUpdateRequest(self, incremental=True):
        """Shortcut for requesting screen update"""
        if not self.initialized:
            return
        (_w, _h) = self.fbDimensions
        self.fbUpdateRequest(0, 0, _w, _h, incremental)

    def keyEvent(self, code, isDown):
        """Send key event to server.

        Parameters:
            code -- keycode as defined in <X11/keysymdef.h>,
            isDown -- true on keydown, false otherwise

        """
        if not self.initialized:
            return
        self.sendData(self.types.pack(
            ("U8", "U8", "x", "x", "U32"),
            (4, 1 if isDown else 0, code)
        ))

    def mouseEvent(self, x, y, buttonStates):
        """Send mouse event to server.

        Parameters:
            x, y -- position of an event
            buttonStates -- mouse button states (max. 8);
                `True` if pressed, `False` if released.

        On a conventional mouse, buttons 1, 2 and 3
        correspond to the left, middle and right buttons on the mouse.

        On a wheel mouse, each step of the wheel upwards is
        represented by a press and release of button 4,
        and each step downwards is represented by a press
        and release of button 5.

        """
        if not self.initialized:
            return
        assert len(buttonStates) <= 8, "Eight buttons only"
        _sentStates = [1 if _el else 0 for _el in buttonStates]
        # ensure that length is eight
        _sentStates.extend((0, )*8)
        # current byte sequence is (left_btn, middle_btn, right_btn, ...)
        # but actually, server expects it in directly opposite order
        _sentStates.reverse()
        # I beleive that it is faster this way.
        # (Haven't checked, though)
        _intState = int("".join(map(str, _sentStates)), 2)
        self.sendData(self.types.pack(
            ("U8", "U8", "U16", "U16"),
            (5, _intState, x, y)
        ))

    def clientCutText(self, text):
        """The client has new text in its cut buffer.

        There is currently no way to transfer text
        outside the Latin-1 character set.

        """
        if not self.initialized:
            return
        _data = text.encode("latin1")
        self.sendData(self.types.pack(
            ("U8", "x", "x", "x", "U32"),
            (6, len(_data))
        ))
        self.sendData(_data)

    #server-to-client events

    def onBell(self):
        """Server sends 'bell' event."""
        self.log("Bom-bom.")

    def onServerCutText(self, text):
        """The server has new text in its cut buffer."""
        self.log("Server has new text: %r" % text)

    def onSetColourMapEntries(self, colourMap):
        """Set colour map entries message received."""
        self.log("Set colour map entries: %s" % colourMap)

    def copyRect(self, srcX, srcY, width, height, x, y):
        """Copy rectangle from given positions to given positions."""
        self.log("Copy rectangle (%sx%s) (%s:%s)->(%s:%s)" % (
            width, height, srcX, srcY, x, y))

    def getState(self, name):
        """Return state instance for given state name.

        (Used by plug-in states)
        """
        if name == "onAuthCompleted":
            return initStates.AuthCompleted()
        else:
            raise exceptions.RfbException("Unknwon state name %r" % name)

    # internal method called by encodings
    def setRawRect(self, rect):
        rect.pop("data")
        self.log("Got rect %s data" % rect)

    def copyRect(self, rect):
        self.log("Copy rect %s" % rect)

    def fillRect(self, rect):
        self.log("Called fill on %s rect" % rect)

    def setCursor(self, rect, cursor, mask):
        self.log("Got cursor data.")

# vim: set sts=4 sw=4 et :
